Dans le tutoriel TIMFlix, nous avons construit une application qui affichait une liste de vidéos à l’intérieur d’un UITableView. Les informations des vidéos, ainsi que l’image des affiches, étaient renseignées localement grace à un fichier de propriétés.
L’inconvénient de cette approche est évident lorsque vient le temps d’y ajouter de nouvelles vidéos.
Dans ce tutoriel, nous verrons comment:
Vidéo du résultat final:
http://www.youtube.com/watch?v=LBi6Z0RnINM
Le site ‘RottenTomatoes’ propose une API d’accès aux données qu’il compile. Ces données nous renseignent sur des films, les critiques, les acteurs, les personnages, …
Pour interroger l’api de rottentomatoes, il suffit de suivre l’URL suivante:
http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=VOTRECLE&q=MODELE_DE_RECHERCHE&page_limit=NB_PAGES
Comme par exemple, http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=ax2345bv67ncs2734&q=Star&page_limit=10
Note: La clé de l’exemple est invalide. Vous devez la remplacer par celle que vous avez obtenue de rottentomatoes. La limite de pages fixée par l’api est de 50 pages.
La requête précédente devrait nous retourner une structure JSON contenant une liste d’au plus 10 films dont la chaîne ‘Star’ apparaît dans le titre du film.
Pour obtenir, à partir du réseau Internet, des données en format JSON et créer un dictionnaire, il faut utiliser la syntaxe suivante:
donnesViaURL = [NSData dataWithContentsOfURL:[NSURL URLWithString:adresseURL]];
NSError *error;
structureJSONConvertie = [NSJSONSerialization JSONObjectWithData:donnesViaURL options:kNilOptions error:&error];
ACTION – Ouvrons le projet de départ et ajoutons le code suivant à la méthode ‘rechercherFilms’ de la classe contrôleur de la scène principale.
// ViewController.m // -------------------------------------------------------- -(BOOL) rechercherFilms:(NSString *) film // -------------------------------------------------------- // Description: Méthode qui ... { // Construire l'URL pointant sur l'API NSString * URLFilm = [NSString stringWithFormat:@"http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=%@&q=%@&page_limit=%@", CLE_API,film, NB_FILMS]; // Obtenir les données en format brut NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:URLFilm]]; // Si data est vide alors quitter la méthode if (!data) { NSLog(@"URL n'a pas retourné de données!"); return NO;} // Convertir les données brutes en format JSON et les placer // dans tableauDesFilms NSError *error; tableauDesFilms = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; // Vérifier si la conversion a fonctionné if (!tableauDesFilms) { NSLog(@"Pour une raison étrange, la requête chez 'rottentomatoes' a retournée une structure JSON mal formée..."); return NO; } NSLog(@"tableauDesFilms = %@", tableauDesFilms); self.nbResultat.text = [NSString stringWithFormat:@"%d", ((NSArray*)tableauDesFilms[@"movies"]).count]; return YES; } // FIN -> rechercherFilms
ACTION – Testons l’application. Remarquez que la méthode ‘rechercherFilm’ est appelée dans ‘viewDidLoad’.
Nous devrions obtenir le résultat suivant dans la zone ‘debug » d’Xcode:
tableauDesFilms = { "link_template" = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q={search-term}&page_limit={results-per-page}&page={page-number}"; links = { next = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q=star&page_limit=1&page=2"; self = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q=star&page_limit=1&page=1"; }; movies = ( // <-- tableau de films { // <-- dictionnaire de données du premier film de la liste "abridged_cast" = ( { characters = ( "Anakin Skywalker/Darth Vader" ); id = 162652153; name = "Hayden Christensen"; }, { characters = ( "Obi-Wan Kenobi" ); id = 162652152; name = "Ewan McGregor"; }, { characters = ( "R2-D2" ); id = 418638213; name = "Kenny Baker"; }, { characters = ( "Ruwee Naberrie" ); id = 548155708; name = "Graeme Blundell"; }, { characters = ( "Captain Colton" ); id = 358317901; name = "Jeremy Bulloch"; } ); "alternate_ids" = { imdb = 0121766; }; "critics_consensus" = "This sixth and final installment of George Lucas' epic space opera will please die-hard fanatics and non-believers alike -- largely due to awesome digital effects and the sheer power of the mythology."; id = 9; links = { alternate = "http://www.rottentomatoes.com/m/star_wars_episode_iii_revenge_of_the_sith_3d/"; cast = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/cast.json"; clips = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/clips.json"; reviews = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/reviews.json"; self = "http://api.rottentomatoes.com/api/public/v1.0/movies/9.json"; similar = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/similar.json"; }; "mpaa_rating" = "PG-13"; posters = { detailed = "http://content8.flixster.com/movie/10/94/47/10944718_det.jpg"; original = "http://content8.flixster.com/movie/10/94/47/10944718_ori.jpg"; profile = "http://content8.flixster.com/movie/10/94/47/10944718_pro.jpg"; thumbnail = "http://content8.flixster.com/movie/10/94/47/10944718_mob.jpg"; }; ratings = { "audience_rating" = Upright; "audience_score" = 65; "critics_rating" = "Certified Fresh"; "critics_score" = 80; }; "release_dates" = { dvd = "2005-11-01"; }; runtime = 140; synopsis = ""; title = "Star Wars: Episode III - Revenge of the Sith 3D"; year = 2005; // <-- date du premier film de la liste }, // <-- fin des données du premier film de la liste { // <-- deuxième film de la liste "abridged_cast" = (); ... "mpaa_rating" = "PG-13"; posters = { original = "http://content9.flixster.com/movie/11/17/38/11173843_ori.jpg"; }; runtime = 127; synopsis = ""; title = "Star Trek"; year = 2009; }, ... ); total = 1319; }
En analysant la structure précédente, il est possible d’énoncer les axiomes suivants:
Ainsi, pour obtenir la date du premier film de la liste nous écrirons:
dateDuPremierFilm = dictionnaire[@ »movies »][0][@ »year »]
QUESTION: Comment obtenir l’affiche originale du deuxième film?
ACTION – Affichons le titre des films reçus (à la fin de la méthode: ‘rechercherFilms’)
:
// Corriger l'erreur avec une signature pour tableauDesFilms[@"movies"] for (int i = 0 ; i< tableauDesFilms[@"movies"].count; i++){ NSLog(@"Titre %d = %@", i, tableauDesFilms[@"movies"][i][@"title"]); }
Remarquer qu’il y a une zone de recherche en haut à droite de l’application. Nous allons utiliser la méthode de délégation ‘textFieldShouldReturn’ du ‘UITextField’ pour relancer la requête vers l’api de rottenTomatoes.
ACTION – Ajoutons le code suivant à la méthode ‘textFieldShouldReturn’ de la classe contrôleur de la scène principale.
- (BOOL)textFieldShouldReturn:(UITextField *)textField{ // Convertir la chaine en format 'escaped' pour le web, par exemple, remplacer les espaces par %20, ... NSString *escapedText = [textField.text stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSLog(@"escapedText = %@", escapedText); // Relancer la requete vers rottenTomatoes [self rechercherFilms:escapedText]; // Actualiser les cellules du UICollectionView [self.collectionDeFilms reloadData]; // Arreter l'animation de progression [self.progression stopAnimating]; [textField resignFirstResponder]; return YES; }
ACTION – Remplaçons la trace sur le titre des films par le code suivant (méthode ‘rechercherFilms’). Nous allons ici, utiliser l’énumération rapide pour parcourir les éléments de ‘tableauDesFilms[@ »movies »]’.
-(BOOL) rechercherFilms:(NSString *) film ... // Afficher le titre de tous les films trouvés for (NSDictionary * film in tableauDesFilms[@"movies"]) { NSLog(@"Titre = %@", film[@"title"]); } return ...
ACTION – Testons l’application
http://www.youtube.com/watch?v=SgfEfEuKYrU
Nous savons qu’il est possible de créer un objet de type ‘UIImage’ à partir du nom d’un fichier livré avec l’application de la façon suivante:
[UIImage imageNamed:@ »pochette.jpg »];
Mais lorsque que ce fichier est stocké dans le réseau Internet il faut procéder ainsi:
[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:urlImage]]]
Note: La qualité de l’expérience utilisateur va être affectée par le chargement d’un grand nombre d’images. Plus tard, nous vous proposerons une solution à ce problème.
ACTION – Ajoutons le code suivant à la méthode ‘cellForItem…’ du fichier ViewController.m »
// ViewController.m - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ CelluleRotten * celluleCourante = [collectionView dequeueReusableCellWithReuseIdentifier:@"modeleCellule" forIndexPath:indexPath]; NSString * urlImage = tableauDesFilms[@"movies"][indexPath.row][@"posters"][@"original"]; // Cette opération est très bloquante!!! // Charger une image à partir du web // À compléter ... celluleCourante.filmImage.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:urlImage]]]; // Renseigner le titre du film // À compléter ... celluleCourante.titre.text = tableauDesFilms[@"movies"][indexPath.row][@"title"]; return celluleCourante; }
ACTION – Corrigeons la valeur de retour de la méthode ‘numberOfItems…’
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return ((NSArray*)tableauDesFilms[@"movies"]).count; }
ACTION – Ajoutons le code suivant à la méthode ‘prepareForSegue’
// ViewController.m // Passer les info à la scène de détail -(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { DetailFilmViewController * vc = [segue destinationViewController]; // Passer les informations de la section courant à la scène de détail // À compléter... vc.detailFilm = tableauDesFilms[@"movies"][[[self.ViewVideos indexPathForCell:sender] row]]; }
ACTION – Augmentons le nombre maximum de films pour la requête vers l’api de rottenTomatoes
// ViewController.h #define NB_FILMS @"50"
ACTION – Testons l’application. Vous allez remarquer un important manque de réponse de l’application.
http://www.youtube.com/watch?v=bsFFn6ULjyI
Télécharger le projet à cette étape
Présentement, les requêtes web de l’application sont bloquantes, C-A-D que l’application attend la réponse de la requête avant de passer à la ligne de code suivante. Cela a pour effet de bloquer aussi l’interactivité de l’application.
Le ‘framework’ cocoa nous propose l’objet ‘NSURLConnection‘ et la méthode suivante pour lancer une requête web non bloquante:
[NSURLConnection
sendAsynchronousRequest:UneRequete
queue: uneFileExecution
completionHandler:UnBlocDeCodeAExecuter // à la réception des données
]
Présentement, nous utilisons la syntaxe suivante pour lancer une requête vers l’api de rottenTomatoes:
... // Obtenir les données en format brut NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:URLFilm]]; ... // Convertir les données brutes en format JSON tableauDesFilms = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; ...
ACTION – Modifions la méthode ‘rechercherFilms’ pour rendre la requête non bloquante
// ViewController.m // -------------------------------------------------------- -(BOOL) rechercherFilms:(NSString *) film // -------------------------------------------------------- // Description: Méthode qui ... { [self.progression startAnimating]; NSString * adresseURL = [NSString stringWithFormat:@"http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=%@&q=%@&page_limit=%@", CLE_API,film, NB_FILMS]; // Requete via URL non bloquante [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:adresseURL]] queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if (!error) { NSLog(@"Requete = succes"); tableauDesFilms = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; self.nbResultat.text = [NSString stringWithFormat:@"%d",((NSArray*)tableauDesFilms[@"movies"]).count]; [self.collectionDeFilms reloadData]; [self.progression stopAnimating]; } else { NSLog(@"Oups, problème avec la requête web, erreur = %@", error); } // FIN -> if, else } // FIN -> completionHandler ]; return YES; } // FIN -> rechercherFilms
ACTION – Testons l’application.
Vous allez remarquer que la scène principale est affichée immédiatement. Précédemment, la scène était affichée suite au traitement de la requête web qui bloquait l’exécution de l’application.
Par contre, le chargement des pochettes de films est encore bloquant.
Utilisons la même technique pour charger les images via le réseau Internet.
ACTION – Dans la méthode ‘cellForItemAtIndexPath‘, remplaçons la ligne de code qui charge l’image via une URL par:
// ViewController.m - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ ... // Cette opération n'est plus bloquante!!! // Charger une image à partir du web // celluleCourante.filmImage.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:urlImage]]]; NSMutableURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlImage]]; celluleCourante.filmImage.image = [UIImage imageNamed:@"loading.gif"]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if ( !error ) { celluleCourante.filmImage.image = [[UIImage alloc] initWithData:data]; } }]; ... }
ACTION – À vous de modifier la classe de la scène ‘détail’ pour que le chargement des quatre affiches ne soit plus bloquant.
Télécharger le projet terminé
Voici une liste d’APIs disponibles via le web:
http://www.programmableweb.com/apis/directory
http://query.yahooapis.com/v1/public/yql?q=select%20item%20from%20weather.forecast%20where%20location%3D%22CAXX0301%22&format=json
Ce qui donne la météo pour Montréal (CAXX0301)
Voir: http://developer.yahoo.com/yql/
http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20yahoo.finance.quotes%20where%20symbol%20in%20%28%22AAPL,AMZN%22%29&env=store://datatables.org/alltableswithkeys&format=json
Ce qui donne les cotes d’Apple et Amazon
http://api.tou.tv/v1/toutvapiservice.svc/json/GetPageRepertoire
Ce qui donne le répertoire de tou.tv
Voir: https://code.google.com/p/tou-tv-for-boxee/wiki/Api
Note: L’image sur cette page est la propriété intellectuelle de Flixster.